logo头像
Snippet 博客主题

Hadoop源码学习-脚本命令(hadoop fs -ls)执行细节

** Hadoop源码学习-脚本命令(hadoop fs -ls)执行细节:** <Excerpt in index | 首页摘要>

​ Spark学习之路 (一)Spark初识

<The rest of contents | 余下全文>

Hadoop有提供一些脚本命令,以便于我们对HDFS进行管理,可以通过命令hadoop fs进行查看:

通过以上使用说明可以发现,里面提供了大多数和我们在本地操作文件系统相类似的命令,例如,cat查看文件内容,chgrp改变用户群组权限,chmod改变用户权限,chown改变用户拥有者权限,还有创建目录,查看目录,移动文件,重命名等等。

hadoop fs -ls
这里,我们来看看命令hadoop fs -ls:

这个命令大家肯定非常熟悉,在Linux下使用超级频繁,功能就是列出指定目录下的文件及文件夹。那接下来,我们来看看它是怎样查找到此目录下的文件及文件夹的。

在运行hadoop fs -ls /命令之后,真正的命令只是hadoop,后面只是参数,那么,这个hadoop命令到底是哪呢?如果说集群是自己手动搭配的话,那大家肯定知道,这个命令就在${HADOOP_HOME}/bin目录下,但如果集群是自动化部署的时候,你可能一下子找不到这个命令在哪,此时,可以使用以下命令查找:

可以看到,这个命令应该是在目录/usr/bin/之下,使用vim /usr/bin/hadoop查看命令详细:

1
2
3
4
5
6
7
8
9
10
#!/bin/bash

export HADOOP_HOME=${HADOOP_HOME:-/usr/hdp/2.5.0.0-1245/hadoop}
export HADOOP_MAPRED_HOME=${HADOOP_MAPRED_HOME:-/usr/hdp/2.5.0.0-1245/hadoop-mapreduce}
export HADOOP_YARN_HOME=${HADOOP_YARN_HOME:-/usr/hdp/2.5.0.0-1245/hadoop-yarn}
export HADOOP_LIBEXEC_DIR=${HADOOP_HOME}/libexec
export HDP_VERSION=${HDP_VERSION:-2.5.0.0-1245}
export HADOOP_OPTS="${HADOOP_OPTS} -Dhdp.version=${HDP_VERSION}"

exec /usr/hdp/2.5.0.0-1245//hadoop/bin/hadoop.distro "$@"

这个脚本做了两件事,一是export了一些环境变量,使得之后运行的子程序都可以共享这些变量的值;二是执行了命令hadoop.distro命令,并传上了所有参数。

现在,我们来看下命令hadoop.distro做了哪些事,由于代码有点小多,我就不全部贴了,只贴与执行命令hadoop fs -ls /相关的代码。

命令hadoop.distro做的事情是:根据之前传入的参数,然后做一些判断,确定一些变量的值,最后执行的是以下命令:

exec “$JAVA” $JAVA_HEAP_MAX $HADOOP_OPTS $CLASS “$@”
1
这里,我们看到了一堆变量,其中
$JAVA:java
$JAVA_HEAP_MAX:
$HADOOP_OPTS:

# Always respect HADOOP_OPTS and HADOOP_CLIENT_OPTS
HADOOP_OPTS="$HADOOP_OPTS $HADOOP_CLIENT_OPTS"

#make sure security appender is turned off
HADOOP_OPTS="$HADOOP_OPTS -Dhadoop.security.logger=${HADOOP_SECURITY_LOGGER:-INFO,NullAppender}"

$CLASS:org.apache.hadoop.fs.FsShell
$@ : -ls / 注意:这里已经没有参数fs了

因此,命令hadoop.distro也就转换成执行一个JAVA类了,然后继续带上参数。

打开hadoop的源代码,找到类org.apache.hadoop.fs.FsShell,它的main方法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public static void main(String argv[]) throws Exception {
FsShell shell = newShellInstance(); //创建FsShell实例
Configuration conf = new Configuration(); //配置类,
conf.setQuietMode(false); //设置成“非安静模式”,默认为“安静模式”,在安静模式下,error和information的信息不会被记录。

shell.setConf(conf);
int res;
try {
res = ToolRunner.run(shell, argv); //ToolRunner就是一个工具类,用于执行实现了接口Tool的类
} finally {
shell.close();
}
System.exit(res);
}

ToolRunner类结合GenericOptionsParser类来解析命令行参数,
在运行上述ToolRunner.run(shell, argv)代码之后,经过一番解释之后,最后真正执行的仍然是类FsShell的run方法,而且对其参数进行了解析,run方法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
  @Override
public int run(String argv[]) throws Exception {
// initialize FsShell 包括注册命令类
init();
int exitCode = -1;
if (argv.length < 1) {
printUsage(System.err); //打印使用方法
} else {
String cmd = argv[0]; //取到第一个参数,即 ls
Command instance = null;
try {
// 取得实现了该命令(ls)的命令实例,并且此类已经通过类CommandFactory的addClass方法进行了注册
instance = commandFactory.getInstance(cmd);
if (instance == null) {
throw new UnknownCommandException();
}
exitCode = instance.run(Arrays.copyOfRange(argv, 1, argv.length));
} catch (IllegalArgumentException e) {
displayError(cmd, e.getLocalizedMessage());
if (instance != null) {
printInstanceUsage(System.err, instance);
}
} catch (Exception e) {
// instance.run catches IOE, so something is REALLY wrong if here
LOG.debug("Error", e);
displayError(cmd, "Fatal internal error");
e.printStackTrace(System.err);
}
}
return exitCode;
}

注意:通过查看类Ls的代码,我们可以发现,它有一个静态方法registerCommands,这个方法就是对类Ls进行注册,但是,这只是一个静态方法,那么,到底是在哪进行了此方法的调用呢?

1
2
3
4
5
class Ls extends FsCommand {
public static void registerCommands(CommandFactory factory) {
factory.addClass(Ls.class, "-ls");
factory.addClass(Lsr.class, "-lsr");
}

细心的朋友可能已经发现,就在类FsShell的run方法中,调用了一个init方法,而就在此方法中,有一行注册命令的代码,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
protected void init() throws IOException {
getConf().setQuietMode(true);
if (commandFactory == null) {
commandFactory = new CommandFactory(getConf());
commandFactory.addObject(new Help(), "-help");
commandFactory.addObject(new Usage(), "-usage");
// 注册,调用registerCommands方法
registerCommands(commandFactory);
}
}

protected void registerCommands(CommandFactory factory) {
// TODO: DFSAdmin subclasses FsShell so need to protect the command
// registration. This class should morph into a base class for
// commands, and then this method can be abstract
if (this.getClass().equals(FsShell.class)) {
// 调用CommandFactory类的registerCommands方法
// 注意,这里传的参数是类FsCommand
factory.registerCommands(FsCommand.class);
}
}

CommandFactory类的registerCommands方法如下:

1
2
3
4
5
6
7
8
9
10
public void registerCommands(Class<?> registrarClass) {
try {
// 这里触发的是类CommandFactory的registerCommands方法
registrarClass.getMethod(
"registerCommands", CommandFactory.class
).invoke(null, this);
} catch (Exception e) {
throw new RuntimeException(StringUtils.stringifyException(e));
}
}

接下来,我拉看看类CommandFactory的registerCommands方法,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public static void registerCommands(CommandFactory factory) {
factory.registerCommands(AclCommands.class);
factory.registerCommands(CopyCommands.class);
factory.registerCommands(Count.class);
factory.registerCommands(Delete.class);
factory.registerCommands(Display.class);
factory.registerCommands(Find.class);
factory.registerCommands(FsShellPermissions.class);
factory.registerCommands(FsUsage.class);
// 我们会用到的就是这个类Ls
factory.registerCommands(Ls.class);
factory.registerCommands(Mkdir.class);
factory.registerCommands(MoveCommands.class);
factory.registerCommands(SetReplication.class);
factory.registerCommands(Stat.class);
factory.registerCommands(Tail.class);
factory.registerCommands(Test.class);
factory.registerCommands(Touch.class);
factory.registerCommands(Truncate.class);
factory.registerCommands(SnapshotCommands.class);
factory.registerCommands(XAttrCommands.class);
}

我们再来看看Ls类

1
2
3
4
5
class Ls extends FsCommand {
public static void registerCommands(CommandFactory factory) {
factory.addClass(Ls.class, "-ls");
factory.addClass(Lsr.class, "-lsr");
}

也就是,在调用init方法的时候,对这些命令类进行了注册。
因此,上面的那个instance,在这里的话,其实就是类Ls的实例。类Ls继承类FsCommand,而类FsCommand是继承类Command,前面instance调用的run方法其实是父类Command的run方法,此方法主要做了两件事,一是处理配置选项,如-d,-R,-h,二是处理参数,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public int run(String...argv) {
LinkedList<String> args = new LinkedList<String>(Arrays.asList(argv));
try {
if (isDeprecated()) {
displayWarning(
"DEPRECATED: Please use '"+ getReplacementCommand() + "' instead.");
}
processOptions(args);
processRawArguments(args);
} catch (IOException e) {
displayError(e);
}
return (numErrors == 0) ? exitCode : exitCodeForError();
}

方法processRawArguments的调用层次关系如下:

\-> processRawArguments(LinkedList)
     |-> expandArguments(LinkedList)
     |   \-> expandArgument(String)*
     \-> processArguments(LinkedList)
         |-> processArgument(PathData)*
         |   |-> processPathArgument(PathData)
         |   \-> processPaths(PathData, PathData...)
         |        \-> processPath(PathData)*
         \-> processNonexistentPath(PathData)

从这个层次关系中可以看出,整个方法是先进行展开参数,传入的参数是LinkedList,展开后的参数是LinkedList,PathData类中包含了Path,FileStatus,FileSystem。其实,当程序运行到这里的时候,命令ls的结果就已经可以通过类PathData中的相关方法获取了。

展开参数后,开始进行处理参数,此时的参数就是LinkedList,然后循环处理此List,先是判断目录是否存在,是否需要递归查找,是否只是列出本目录(就是看有没有-R和-d参数),我们来看一下到底是如何输出结果的:

1
2
3
4
5
6
7
8
9
@Override
protected void processPaths(PathData parent, PathData ... items)
throws IOException {
if (parent != null && !isRecursive() && items.length != 0) {
out.println("Found " + items.length + " items");
}
adjustColumnWidths(items); // 计算列宽,重新构建格式字符串
super.processPaths(parent, items);
}

看到这里,大家是不是觉得很面熟?没想起来?我们上个图:

这下看到了吧,最是输出结果的第一行,找到11项。

接下来重新调整了一下列宽,最后调用了父类的processPaths方法,我们继续来看父类的这个方法,它做了哪些事:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
protected void processPaths(PathData parent, PathData ... items)
throws IOException {
// TODO: this really should be iterative
for (PathData item : items) {
try {
processPath(item); // 真正处理每一项,然后打印出来
if (recursive && isPathRecursable(item)) {
recursePath(item); // 如果有指定参数 -R,则需要进行递归
}
postProcessPath(item); // 这个没理解,DFS还有后序DFS么?有知情者,请告知,谢谢。
} catch (IOException e) {
displayError(e);
}
}
}

我们来看一下打印具体每行信息的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Override
protected void processPath(PathData item) throws IOException {
FileStatus stat = item.stat;
String line = String.format(lineFormat,
(stat.isDirectory() ? "d" : "-"), // 文件夹显示d,文件显示-
stat.getPermission() + (stat.getPermission().getAclBit() ? "+" : " "), // 获取权限
(stat.isFile() ? stat.getReplication() : "-"),
stat.getOwner(), // 获取拥有者
stat.getGroup(), // 获取组
formatSize(stat.getLen()), // 获取大小
dateFormat.format(new Date(stat.getModificationTime())), // 日期
item // 项,即路径
);
out.println(line); // 打印行
}

到这里,命令hadoop fs -ls /的执行过程基本已经结束(关于文件系统内部细节,后续再讲),这就是整个命令执行的过程。最后,我们来总结一下:

执行shell。执行命令hadoop fs -ls /,首先执行的是shell命令,然后转换成执行Java类。
执行Java。在执行Java类的时候,使用工具类对其进行配置项解析,并使用反射机制对命令进行了转换,于是后面变成了调用类Ls的run方法。
调用类Ls的相关方法。类Ls负责处理路径,并打印详情。

原文链接:https://blog.csdn.net/strongyoung88/article/details/68952248